package org.xx.armory.junit;

import org.hamcrest.BaseMatcher;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import static java.util.Arrays.asList;

public final class Matchers {
    @SuppressWarnings("unused")
    private Matchers() {
        throw new AssertionError();
    }

    /**
     * 检查指定的 {@link CharSequence} 对象是否具有指定的长度。
     *
     * @param expected
     *         期望的长度。
     * @return 检查对象。
     */
    public static Matcher<Object> hasLength(
            int expected
    ) {
        return new CharSequenceLengthMatcher(expected);
    }

    public static Matcher<Collection<?>> collectionEqualTo(
            Collection<?> expected
    ) {
        return new CollectionEqualToMatcher(expected);
    }

    public static Matcher<Collection<?>> collectionEqualTo(
            Object... expected
    ) {
        return new CollectionEqualToMatcher(asList(expected));
    }

    public static Matcher<Map<?, ?>> mapEqualTo(
            Map<?, ?> expected
    ) {
        return new MapEqualToMatcher(expected);
    }

    private static Matcher<?> createMatcher(
            Object expected
    ) {
        if (expected == null) {
            return CoreMatchers.nullValue();
        } else if (expected instanceof Number) {
            return new NumberEqualToMatcher((Number) expected);
        } else if (expected instanceof Collection<?>) {
            return new CollectionEqualToMatcher((Collection<?>) expected);
        } else if (expected instanceof Map<?, ?>) {
            return new MapEqualToMatcher((Map<?, ?>) expected);
        } else {
            return CoreMatchers.equalToObject(expected);
        }
    }

    /**
     * 检查指定的 {@link Number} 对象是否具有指定的值。
     *
     * <p>此检测使用只执行数学意义上的比较，而不考虑精度和类型。</p>
     * <p>如果待检查值的精度比{@code expected}更高，那么截断到和{@code expected}同样的精度，然后再比较。<strong>这个过程中可能会出现舍去截断。</strong></p>
     * <p>如果待检查值和{@code expected}都是{@link BigDecimal}，那么使用{@link BigDecimal#compareTo(BigDecimal)}方法进行比较。</p>
     *
     * @param expected
     *         期望的值。
     * @return 检查对象。
     */
    public static Matcher<Object> numberEqualTo(
            Number expected
    ) {
        return new NumberEqualToMatcher(expected);
    }

    private static class CharSequenceLengthMatcher
            extends BaseMatcher<Object> {
        private final int length;

        public CharSequenceLengthMatcher(
                int length
        ) {
            this.length = length;
        }

        @Override
        public boolean matches(
                Object item
        ) {
            if (!(item instanceof final CharSequence actual)) {
                return false;
            }

            return actual.length() == this.length;
        }

        @Override
        public void describeMismatch(
                Object item,
                Description mismatchDescription
        ) {
            if (!(item instanceof CharSequence)) {
                mismatchDescription.appendText(" is not CharSequence");
            } else {
                mismatchDescription.appendValue(item).appendText(" has length ").appendValue(((CharSequence) item).length());
            }
        }

        @Override
        public void describeTo(
                Description description
        ) {
            description.appendText("Char sequence has length ").appendValue(this.length);
        }
    }

    private static class NumberEqualToMatcher
            extends BaseMatcher<Object> {
        private final Number expected;

        public NumberEqualToMatcher(
                Number expected
        ) {
            this.expected = Objects.requireNonNull(expected, "Argument \"expected\" must not be null");
        }

        @Override
        public boolean matches(
                Object item
        ) {
            if (!(item instanceof final Number actual)) {
                return false;
            }

            if (this.expected instanceof BigDecimal) {
                // 将 actual 转化为 BigDecimal，并去掉尾部零 再比较。
                BigDecimal actual_;
                if (actual instanceof BigDecimal) {
                    actual_ = (BigDecimal) actual;
                } else if (actual instanceof Double || actual instanceof Float) {
                    actual_ = BigDecimal.valueOf(actual.doubleValue());
                } else {
                    actual_ = BigDecimal.valueOf(actual.longValue());
                }

                return actual_.compareTo((BigDecimal) this.expected) == 0;
            } else if (this.expected instanceof Double || this.expected instanceof Float) {
                // 将 actual 转化为 double 再比较。
                return actual.doubleValue() == this.expected.doubleValue();
            } else {
                // 将 actual 转化为 long 再比较。
                return actual.longValue() == this.expected.longValue();
            }
        }

        @Override
        public void describeMismatch(
                Object item,
                Description mismatchDescription
        ) {
            if (!(item instanceof Number)) {
                mismatchDescription.appendText(" is not Number");
            } else {
                mismatchDescription.appendValue(item);
            }
        }

        @Override
        public void describeTo(
                Description description
        ) {
            description.appendText("Number equals to ").appendValue(this.expected);
        }
    }

    private static class CollectionEqualToMatcher
            extends BaseMatcher<Collection<?>> {
        private final Collection<?> expected;

        private int index;
        private Object value;
        private Matcher<?> matcher;

        public CollectionEqualToMatcher(
                Collection<?> expected
        ) {
            this.expected = new ArrayList<>(Objects.requireNonNull(expected, "Argument \"expected\" must not be null"));
        }

        @Override
        public boolean matches(
                Object o
        ) {
            if (o instanceof Collection<?>) {
                final var iter0 = this.expected.iterator();
                final var iter1 = ((Collection<?>) o).iterator();
                var index = 0;
                while (iter0.hasNext()) {
                    if (iter1.hasNext()) {
                        final var matcher = createMatcher(iter0.next());
                        final var value = iter1.next();
                        if (!matcher.matches(value)) {
                            this.matcher = matcher;
                            this.index = index;
                            this.value = value;
                            return false;
                        }
                    } else {
                        this.matcher = null;
                        this.index = index;
                        this.value = null;
                        return false;
                    }
                    ++index;
                }
                return !iter1.hasNext();
            } else {
                return false;
            }
        }

        @Override
        public void describeMismatch(
                Object item,
                Description mismatchDescription
        ) {
            if (!(item instanceof Collection<?>)) {
                mismatchDescription.appendText(" is not a collection");
            } else {
                if (this.matcher != null) {
                    mismatchDescription.appendText("[" + this.index + "] is ").appendValue(value);
                } else {
                    mismatchDescription.appendText("Collection has size: ").appendValue(this.index);
                }
            }
        }

        @Override
        public void describeTo(
                Description description
        ) {
            if (this.matcher != null) {
                description.appendText("[" + this.index + "] should match ").appendDescriptionOf(this.matcher);
            } else {
                description.appendText("Collection has size: ").appendValue(this.expected.size());
            }
        }
    }

    private static class MapEqualToMatcher
            extends BaseMatcher<Map<?, ?>> {
        private final Map<?, ?> expected;

        private Object key;
        private Object value;
        private Matcher<?> matcher;

        public MapEqualToMatcher(
                Map<?, ?> expected
        ) {
            this.expected = new LinkedHashMap<>(Objects.requireNonNull(expected, "Argument \"expected\" must not be null"));
        }

        @Override
        public boolean matches(
                Object o
        ) {
            if (o instanceof Map<?, ?>) {
                for (final var entry : this.expected.entrySet()) {
                    final var ov = ((Map<?, ?>) o).get(entry.getKey());
                    final var matcher = createMatcher(entry.getValue());
                    if (!matcher.matches(ov)) {
                        this.key = entry.getKey();
                        this.value = ov;
                        this.matcher = matcher;
                        return false;
                    }
                }
            }
            return true;
        }

        @Override
        public void describeMismatch(
                Object item,
                Description mismatchDescription
        ) {
            if (!(item instanceof Map<?, ?>)) {
                mismatchDescription.appendText(" is not a map");
            } else {
                mismatchDescription.appendText("[" + this.key + "] is ").appendValue(this.value);
            }
        }

        @Override
        public void describeTo(
                Description description
        ) {
            description.appendText("[" + this.key + "] should match ").appendDescriptionOf(this.matcher);
        }
    }
}
